package app.species;

import sim.engine.*;
import sim.field.continuous.*;
import sim.util.*;
import ec.util.*;

import java.awt.Color;
import java.io.*;

import app.species.ANN.*;

public class SpeciesSim extends SimState{

	/**
	 * 
	 */
	private static final long serialVersionUID = -4508881767704198011L;
	
	private static final boolean LOG_SIMULATION = false;
	public int fieldWidth, fieldHeight;

	public Continuous2D creatures;
	public Continuous2D stationaryFood;
	public Continuous2D movingFood;
	public Continuous2D predators;
	public Continuous2D turrets;
	public Continuous2D bullets;

	private BestCreatureDisplayer bcd;
	
	private double discretization; // used to make the Continuous2D object

	public static ConfigParams params;
	transient private PrintWriter statsFile;

	protected int numSpecies;

	private int runNum;
	
	public SpeciesSim(long seed, String paramFileName){

		super(new MersenneTwisterFast(seed), new Schedule(1));

		this.params = new ConfigParams(paramFileName);

		this.fieldWidth = this.params.getParam("FIELD_WIDTH",this.fieldWidth);
		this.fieldHeight = this.params.getParam("FIELD_HEIGHT",fieldHeight);
		this.discretization = this.params.getParam("DISCRETIZATION",this.discretization);

		this.numSpecies = 0;

	}
	
	public SpeciesSim(long seed)
	{
		this(seed, "run.conf");
	}


	public void start(){

		super.start();

		if(LOG_SIMULATION){
			this.openStatsFile();
		}
		
		// add the schedulers that randomly add in new food
		this.schedule.scheduleRepeating(0, new StationaryFoodCreator(this.params.getParam("STATIONARY_FOOD_SPAWN_RATE",(int)0), 
									     this.params.getParam("STATIONARY_FOOD_SPAWN_POINTS",new SpawnPoint[0])));
		this.schedule.scheduleRepeating(0, new MovingFoodCreator(this.params.getParam("MOVING_FOOD_SPAWN_RATE",(int)0), 
									 this.params.getParam("MOVING_FOOD_SPAWN_POINTS",new SpawnPoint[0])));
		this.schedule.scheduleRepeating(0, new PredatorCreator(this.params.getParam("PREDATOR_SPAWN_RATE",(int)0), 
								       this.params.getParam("PREDATOR_SPAWN_POINTS",new SpawnPoint[0])));

		this.bcd = new BestCreatureDisplayer(this.runNum);
		
		this.schedule.scheduleRepeating(0, this.bcd);

		this.creatures = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);
		this.stationaryFood = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);
		this.movingFood = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);
		this.predators = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);
		this.turrets = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);
		this.bullets = new Continuous2D(this.discretization, this.fieldWidth, this.fieldHeight);

		// maybe this should be done another way, for know, we'll leave it like this
		

//		params.configureClass(CreatureContext.class);
//		params.configureClass(MovingFood.class);
//		params.configureClass(StationaryFood.class);
//		params.configureClass(Creature.class);
		
		
		Axon.setMaxWeight(this.params.getParam("AXON_MAX_WEIGHT",0.0));
		Brain.setAxonWeightMutRate(this.params.getParam("AXON_WEIGHT_MUT_RATE",0.0));
		Brain.setNewSomaMutRate(this.params.getParam("NEW_SOMA_MUT_RATE",0.0));
		Brain.setNewAxonMutRate(this.params.getParam("NEW_AXON_MUT_RATE",0.0));
		Brain.setCompatibilityThreshold(this.params.getParam("COMP_THRESHOLD",0.0));

		CreatureContext.setInputToBrainType(this.params.getParam("CREATURE_BRAIN_INPUT_TYPE",new String()));
		CreatureContext.setCanSeeOtherCreatures(this.params.getParam("CREATURE_CAN_SEE_OTHER_CREATURES",true));
		CreatureContext.setVisionDivisions(this.params.getParam("CREATURE_VISION_DIVISIONS",0));
		CreatureContext.setMatingEnergyThreshold(this.params.getParam("CREATURE_MATING_ENERGY_THRESHOLD",0));
		CreatureContext.setAsexualMatingThreshold(this.params.getParam("CREATURE_ASEXUAL_MATE_THRESHOLD",0));
		CreatureContext.setSharpAngleTurn(this.params.getParam("CREATURE_SHARP_ANGLE_TURN",0.0));
		CreatureContext.setWideAngleTurn(this.params.getParam("CREATURE_WIDE_ANGLE_TURN",0.0));
		CreatureContext.setSpeciesColors(this.params.getParam("CREATURE_SPECIES_COLORS", new Color[0]));
		CreatureContext.setMatingRestriction(this.params.getParam("CREATURE_MATING_RESTRICTION", new String()));
		CreatureContext.setSpeciesSimReference(this);

		StationaryFood.setEnergyAddedToCreature(this.params.getParam("STATIONARY_FOOD_ENERGY_ADDED_TO_CREATURE",0));
		MovingFood.setRandomVectorLength(this.params.getParam("MOVING_FOOD_RANDOM_VECTOR_LENGTH",0.0));
		MovingFood.setEnergyAddedToCreature(this.params.getParam("MOVING_FOOD_ENERGY_ADDED_TO_CREATURE",0));
		Predator.setRandomVectorLength(this.params.getParam("PREDATOR_RANDOM_VECTOR_LENGTH",0.0));
		Predator.setEnergyAddedToCreature(this.params.getParam("PREDATOR_ENERGY_ADDED_TO_CREATURE",0));
		Turret.setShootInterval(this.params.getParam("TURRET_SHOOT_INTERVAL",0));
		SpinningTurret.setShootInterval(this.params.getParam("SPINNING_TURRET_SHOOT_INTERVAL",0));
		Bullet.setInitialEnergy(this.params.getParam("BULLET_INITIAL_ENERGY",0));
		
		this.addToSim(this.creatures, "CREATURE");
		this.addToSim(this.stationaryFood, "STATIONARY_FOOD");
		this.addToSim(this.movingFood, "MOVING_FOOD");
		this.addToSim(this.predators, "PREDATOR");
		this.addToSim(this.turrets, "TURRET");
		this.addToSim(this.turrets, "SPINNING_TURRET");
		this.addToSim(this.bullets, "BULLET");
		System.out.println(params.createCompleteConfigurationFile());

	}

	private void openStatsFile(){

		try{
			BufferedReader br = new BufferedReader(new FileReader("output/run.num"));
			this.runNum = Integer.parseInt(br.readLine());
			
		}catch(IOException e){
			this.runNum = 0;
		}

		try{
			PrintWriter pr = new PrintWriter(new BufferedWriter(new FileWriter("output/run.num")));
			pr.println(this.runNum+1);
			pr.close();
		}catch(IOException e){}

		try{
			this.statsFile = new PrintWriter(new BufferedWriter(new FileWriter("output/run" + this.runNum + ".out")));
			this.statsFile.println(this.statsFileHeader());
		}catch (IOException e){
			e.printStackTrace();
			System.exit(0);
		}

	}

	/* the functions above call this to actually add everything to the simulation
	 **/
	private void addToSim(Continuous2D cont2D, String entityType){

		int numEntities = this.params.getParam(entityType + "_NUM",0);
		SpawnPoint[] spawnPoints = this.params.getParam(entityType + "_SPAWN_POINTS",new SpawnPoint[0]);
		double senseDist = this.params.getParam(entityType + "_SENSE_DISTANCE",0.0);
		double minDiam = this.params.getParam(entityType + "_DIAMETER_MIN",0.0);
		double maxDiam = this.params.getParam(entityType + "_DIAMETER_MAX",0.0);
		double speed = this.params.getParam(entityType + "_SPEED",0.0);
		int lifeSpan = this.params.getParam(entityType + "_LIFE_SPAN",0);
		MutableDouble2D initPosition;
		MutableDouble2D initHeading;
		double diameter;

		if(entityType.equals("CREATURE")){
			Creature.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("STATIONARY_FOOD")){
			StationaryFood.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("MOVING_FOOD")){
			MovingFood.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("PREDATOR")){
			Predator.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("TURRET")){
			Turret.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("SPINNING_TURRET")){
			SpinningTurret.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else if(entityType.equals("BULLET")){
			Bullet.setDefaults(maxDiam, speed, senseDist, lifeSpan);
		}else{
			throw new RuntimeException("Something wrong happened in SpeciesSim when trying to add a new Entity of unknown type.");
		}

		// put the creatures in the simulation
		Entity newEntity;

		for(int i = 0; i < numEntities; i++){

			initPosition = spawnPoints[this.random.nextInt(spawnPoints.length)].getRandomLocation(this.random);

			/* obtain an initial heading
			 **/
			while((initHeading = new MutableDouble2D(this.random.nextDouble(), this.random.nextDouble())).length() > 1.0) ;

			if(this.random.nextBoolean()) initHeading.x *= -1.0;
			if(this.random.nextBoolean()) initHeading.y *= -1.0;

			if(initHeading.lengthSq() > 0.0) initHeading.normalize();

			/* obtain the diameter
			 **/
			diameter = this.random.nextDouble() * (maxDiam-minDiam);
			diameter += minDiam;

			/* create the creature and set all of its parameters
			 * There must be a better way of doing this, for now, this will work
			 **/
			if(entityType.equals("CREATURE")){

				int minEnergy = this.params.getParam("CREATURE_MIN_ENERGY_START",0);
				int maxEnergy = this.params.getParam("CREATURE_MAX_ENERGY_START",0);
				int energy = minEnergy + this.random.nextInt(maxEnergy-minEnergy);

				newEntity = new Creature(initPosition, initHeading, this.createBrain());
				newEntity.setAge(0);
				newEntity.setEnergy(energy);
				
			}else if(entityType.equals("STATIONARY_FOOD")){
				newEntity = new StationaryFood(initPosition, initHeading);
				newEntity.setAge(this.random.nextInt(newEntity.getLifeSpan()));
			}else if(entityType.equals("MOVING_FOOD")){
				newEntity = new MovingFood(initPosition, initHeading);
				newEntity.setAge(this.random.nextInt(newEntity.getLifeSpan()));
			}else if(entityType.equals("PREDATOR")){
				newEntity = new Predator(initPosition, initHeading);
				newEntity.setAge(this.random.nextInt(newEntity.getLifeSpan()));
			}else if(entityType.equals("SPINNING_TURRET")){
				newEntity = new SpinningTurret(initPosition, initHeading);
			}else if(entityType.equals("TURRET")){
				newEntity = new Turret(initPosition, initHeading);
			}else{
				throw new RuntimeException("Something wrong happened in SpeciesSim when trying to add a new Entity of unknown type.");
			}

			newEntity.setDiameter(diameter);
			newEntity.setSpeed(speed);
			newEntity.setSenseDistance(senseDist);

			cont2D.setObjectLocation(newEntity, new Double2D(initPosition));
			newEntity.stoppable = this.schedule.scheduleRepeating(0, newEntity);

		}

	}

	public void finish(){
		if(this.statsFile != null) this.statsFile.close();
		this.bcd.closeFile();
	}

	/* stops the Entity's scheduler and removes it from the Continuous2D
	 **/
	public void removeEntityFromSim(Entity entity, Continuous2D cont2D){

		if(entity instanceof Creature && statsFile!=null){
			Creature c = (Creature)entity;

			statsFile.print(this.schedule.getSteps() + "\t");
			statsFile.print(c.getID() + "\t");
			statsFile.print(c.getAge() + "\t");
			statsFile.print(c.getStationaryFoodEaten() + "\t");
			statsFile.print(c.getMovingFoodEaten() + "\t");
			statsFile.print(c.brain.getSomaChromeLen() + "\t");
			statsFile.print(c.brain.getAxonChromeLen() + "\t");
			statsFile.print(this.creatures.getAllObjects().numObjs + "\t");
			statsFile.print(this.stationaryFood.getAllObjects().numObjs + "\t");
			statsFile.print(this.movingFood.getAllObjects().numObjs + "\t");
			statsFile.println(this.numSpecies);

		}

		entity.setAlive(false);
		entity.stoppable.stop();
		cont2D.remove(entity);

	}

	private String statsFileHeader(){

		String ret;

		ret = "Time\tID\tAge\tS_eat\tM_eat\tS_len\tA_len\tCLive\tSLive\tMLive\tSpcis";

		return ret;

	}

	/* creates a new, fully connected brain
	 **/
	private Brain createBrain(){

		String brainFileName = this.params.getParam("CREATURE_BRAIN_FILE","");
		int divisions = CreatureContext.numVisionDivisions();
		int numInputs = 3;//CreatureContext.numInputsToBrain();
		int numOutputs = 4;//CreatureContext.numDefaultActions();
		String brainType = this.params.getParam("CREATURE_BRAIN_TYPE","");

		if(brainType.equalsIgnoreCase("AdamBrain")){

			if(brainFileName.equals("")) return AdamBrain.makeBrain(numInputs, numOutputs, this.random);
			else			    return AdamBrain.makeBrain(brainFileName, this.random);
		}
//		else if(brainType.equalsIgnoreCase("DavidBrain") || brainType.equalsIgnoreCase("DaveBrain")){
//
//			if(brainFileName.equals("")) return DavidBrain.makeBrain(numInputs, numOutputs, this.random);
//			else			    return DavidBrain.makeBrain(brainFileName, this.random);
//		}
	else{
			throw new RuntimeException(brainType + " brain not found.");
		}

	}

	public static void main(String[] args){
		doLoop(SpeciesSim.class, args);
		System.exit(0);
	}

	// setters and getters
	public int getNumCreatures(){

		if(this.creatures == null) return 0;
		else return this.creatures.getAllObjects().numObjs;

	}

	public int getNumSpecies(){
		return this.numSpecies;
	}

	public double getCompatibilityThreshold(){
		return Brain.getCompatibilityThreshold();
	}
}
